﻿// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace MudBlazor.Docs.Compiler;

/// <summary>
/// Represents a writer for generated API documentation.
/// </summary>
public partial class ApiDocumentationWriter(string filePath) : StreamWriter(File.Create(filePath))
{
    /// <summary>
    /// Creates a new instance with types and the default output path.
    /// </summary>
    public ApiDocumentationWriter() : this(Paths.ApiDocumentationFilePath)
    {
    }

    /// <summary>
    /// Indents generated code to be more readable.
    /// </summary>
    /// <remarks>
    /// Defaults to <c>true</c>.  When <c>false</c>, no code will be indented, which saves space but is less readable.
    /// </remarks>
    public bool EnableIndentation { get; set; } = true;

    /// <summary>
    /// The current indentation level.
    /// </summary>
    public int IndentLevel { get; set; }

    /// <summary>
    /// Writes the copyright boilerplate.
    /// </summary>
    public void WriteHeader()
    {
        WriteLine($"// Copyright (c) MudBlazor {DateTime.Now.Year}");
        WriteLine("// MudBlazor licenses this file to you under the MIT license.");
        WriteLine("// See the LICENSE file in the project root for more information.");
        WriteLine();
        WriteLine("//-----------------------------------------------------------------------");
        WriteLine("// Generated by MudBlazor.Docs.Compiler.ApiDocumentationWriter");
        WriteLine("// Any changes to this file will be overwritten on build");
        WriteLine("// <auto-generated />");
        WriteLine("//-----------------------------------------------------------------------");
        WriteLine();
        WriteLine("using System.Collections.Frozen;");
        WriteLine("using System.Collections.Generic;");
        WriteLine("using System.CodeDom.Compiler;");
        WriteLine();
        WriteLine("namespace MudBlazor.Docs.Models;");
        WriteLine();
    }

    /// <summary>
    /// Writes the start of the ApiDocumentation partial class.
    /// </summary>
    public void WriteClassStart()
    {
        WriteLine("/// <summary>");
        WriteLine("/// Represents all of the XML documentation for public-facing classes.");
        WriteLine("/// </summary>");
        WriteLine($"[GeneratedCodeAttribute(\"MudBlazor.Docs.Compiler\", \"{typeof(ApiDocumentationWriter).Assembly.GetName().Version}\")]");
        WriteLine("public static partial class ApiDocumentation");
        WriteLine("{");
        Indent();
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation partial class.
    /// </summary>
    public void WriteClassEnd()
    {
        Outdent();
        WriteLine("}");
    }

    /// <summary>
    /// Writes a series of tabs to indent the line.
    /// </summary>
    public void WriteIndent()
    {
        if (!EnableIndentation)
        {
            return;
        }

        for (var index = 0; index < IndentLevel; index++)
        {
            Write("\t");
        }
    }

    /// <summary>
    /// Writes the start of the ApiDocumentation constructor.
    /// </summary>
    public void WriteConstructorStart()
    {
        WriteLineIndented("static ApiDocumentation()");
        WriteLineIndented("{");
        Indent();
    }

    /// <summary>
    /// Writes text with the current indentation level.
    /// </summary>
    /// <param name="text">The text to write.</param>
    public void WriteIndented(string text)
    {
        WriteIndent();
        Write(text);
    }

    /// <summary>
    /// Writes text with the current indentation level, and ends the line.
    /// </summary>
    /// <param name="text">The text to write.</param>
    public void WriteLineIndented(string text)
    {
        WriteIndented(text);
        WriteLine();
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation constructor.
    /// </summary>
    public void WriteConstructorEnd()
    {
        Outdent();
        WriteLine("}");
    }

    /// <summary>
    /// Writes the end of the ApiDocumentation class.
    /// </summary>
    public void WriteApiDocumentationClassEnd()
    {
        Outdent();
        WriteLine("}");
    }

    /// <summary>
    /// Increases the indentation level.
    /// </summary>
    public void Indent()
    {
        IndentLevel++;
    }

    /// <summary>
    /// Decreases the indentation level.
    /// </summary>
    public void Outdent()
    {
        IndentLevel--;
    }

    /// <summary>
    /// Formats a string for use in C# code.
    /// </summary>
    /// <param name="code"></param>
    /// <returns></returns>
    public static string Escape(string code) => code?.Replace("\"", "\\\"");

    /// <summary>
    /// Writes the category for the member.
    /// </summary>
    /// <param name="category">The category (derived from <see cref="CategoryAttribute"/>).</param>
    public void WriteCategory(string category)
    {
        if (!string.IsNullOrEmpty(category))
        {
            Write($"Category = \"{category}\", ");
        }
    }

    /// <summary>
    /// Writes the category order.
    /// </summary>
    /// <param name="order">The category order (derived from <see cref="CategoryAttribute"/>).</param>
    public void WriteOrder(int? order)
    {
        if (order.HasValue)
        {
            Write($"Order = {order}, ");
        }
    }

    /// <summary>
    /// Serializes an XML summary for a member.
    /// </summary>
    /// <param name="summary"></param>
    public void WriteSummary(string summary)
    {
        if (!string.IsNullOrEmpty(summary))
        {
            Write($"Summary = \"{Escape(summary)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML summary for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteSummaryIndented(string remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            WriteLineIndented($"Summary = \"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML remarks for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteRemarks(string remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            WriteLine($"Remarks = \"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes an XML remarks for a member.
    /// </summary>
    /// <param name="remarks"></param>
    public void WriteRemarksIndented(string remarks)
    {
        if (!string.IsNullOrEmpty(remarks))
        {
            WriteLineIndented($"Remarks = \"{Escape(remarks)}\", ");
        }
    }

    /// <summary>
    /// Serializes all of the specified types.
    /// </summary>
    /// <param name="types">The types to serialize.</param>
    public void WriteTypes(IDictionary<string, DocumentedType> types)
    {
        WriteLineIndented("// Build all of the documented types");
        WriteLineIndented($"Types = new Dictionary<string, DocumentedType>();");

        foreach (var type in types)
        {
            WriteType(type.Value);
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes the specified type.
    /// </summary>
    /// <param name="type">The type to serialize.</param>
    public void WriteType(DocumentedType type)
    {
        WriteIndented($"Types.Add(\"{type.Key}\", new()");
        WriteLine(" {");
        Indent();
        WriteLineIndented($"Name = \"{type.Name}\", ");
        WriteLineIndented($"NameFriendly = \"{GetFriendlyTypeName(type.Type)}\", ");
        WriteBaseTypeIndented(type.BaseType);
        WriteIsComponentIndented(type.Type.IsSubclassOf(typeof(MudComponentBase)));
        WriteSummaryIndented(type.Summary);
        WriteRemarksIndented(type.Remarks);
        WriteProperties(type);
        WriteGlobalSettings(type);
        WriteFields(type);
        WriteMethods(type);
        Outdent();
        WriteLineIndented("});");
    }

    /// <summary>
    /// Serializes all documented events.
    /// </summary>
    /// <param name="events">The events to write.</param>
    public void WriteEvents(IDictionary<string, DocumentedEvent> events)
    {
        WriteLineIndented("// Build all of the documented events");
        WriteLineIndented($"Events = new Dictionary<string, DocumentedEvent>();");

        foreach (var documentedEvent in events)
        {
            WriteEvent(documentedEvent.Value);
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented event.
    /// </summary>
    /// <param name="documentedEvent">The event to serialize.</param>
    public void WriteEvent(DocumentedEvent documentedEvent)
    {
        WriteIndented($"Events.Add(\"{documentedEvent.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{documentedEvent.Name}\", ");
        Write($"TypeName = \"{documentedEvent.Type.FullName}\", ");
        Write($"TypeFriendlyName = \"{GetFriendlyTypeName(documentedEvent.Type)}\", ");
        WriteDeclaringType(documentedEvent);
        WriteCategory(documentedEvent.Category);
        WriteOrder(documentedEvent.Order);
        WriteSummary(documentedEvent.Summary);
        WriteRemarks(documentedEvent.Remarks);
        Write(" }");
        WriteLine(");");
    }

    /// <summary>
    /// Serializes all documented fields.
    /// </summary>
    /// <param name="fields">The fields to write.</param>
    public void WriteFields(IDictionary<string, DocumentedField> fields)
    {
        WriteLineIndented("// Build all of the documented fields");
        WriteLineIndented($"Fields = new Dictionary<string, DocumentedField>();");

        foreach (var field in fields)
        {
            WriteField(field.Value);
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented field.
    /// </summary>
    /// <param name="field">The field to serialize.</param>
    public void WriteField(DocumentedField field)
    {
        WriteIndented($"Fields.Add(\"{field.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{field.Name}\", ");
        Write($"TypeName = \"{field.Type.FullName}\", ");
        Write($"TypeFriendlyName = \"{GetFriendlyTypeName(field.Type)}\", ");
        WriteDeclaringType(field);
        WriteCategory(field.Category);
        WriteOrder(field.Order);
        WriteSummary(field.Summary);
        WriteRemarks(field.Remarks);
        Write(" }");
        WriteLine(");");
    }

    /// <summary>
    /// Serializes all documented properties.
    /// </summary>
    /// <param name="properties">the properties to write.</param>
    public void WriteProperties(IDictionary<string, DocumentedProperty> properties)
    {
        WriteLineIndented("// Build all of the documented properties");
        WriteLineIndented($"Properties = new Dictionary<string, DocumentedProperty>();");

        foreach (var property in properties)
        {
            if (!ApiDocumentationBuilder.IsExcluded(property.Value.Type))
            {
                WriteProperty(property.Value);
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes a documented property.
    /// </summary>
    /// <param name="property">the property to serialize.</param>
    public void WriteProperty(DocumentedProperty property)
    {
        WriteIndented($"Properties.Add(\"{property.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{property.Name}\", ");
        Write($"TypeName = \"{property.Type.FullName}\", ");
        Write($"TypeFriendlyName = \"{GetFriendlyTypeName(property.Type)}\", ");
        WriteDeclaringType(property);
        WriteCategory(property.Category);
        WriteOrder(property.Order);
        WriteIsParameter(property.IsParameter);
        WriteSummary(property.Summary);
        WriteRemarks(property.Remarks);
        Write(" }");
        WriteLine(");");
    }

    /// <summary>
    /// Serializes the specified properties.
    /// </summary>
    /// <param name="type">The type containing the properties.</param>
    public void WriteProperties(DocumentedType type)
    {
        /* Example:
         
            Properties = { 
				{ "Type.JavaScriptListenerId", Properties["Type.JavaScriptListenerId"], } },
				{ "Type.BrowserWindowSize", Properties["Type.BrowserWindowSize"], } },
				{ "Type.Breakpoint", Properties["Type.Breakpoint"],  } },
				{ "Type.IsImmediate", Properties["Type.IsImmediate"],  } },
            },
          
         */

        // Anything to do?
        if (type.Properties.Count == 0)
        {
            return;
        }

        WriteLineIndented("Properties = { ");
        Indent();

        foreach (var property in type.Properties)
        {
            WriteProperty(type, property.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes the specified MudGlobal settings.
    /// </summary>
    /// <param name="type">The type containing the settings.</param>
    public void WriteGlobalSettings(DocumentedType type)
    {
        /* Example:
         
            GlobalSettings = { 
				{ "JavaScriptListenerId", new() { Type = "Guid", Summary = "Gets the ID of the JavaScript listener.",  } },
				{ "BrowserWindowSize", new() { Type = "BrowserWindowSize", Summary = "Gets the browser window size.",  } },
				{ "Breakpoint", new() { Type = "Breakpoint", Summary = "Gets the breakpoint associated with the browser size.",  } },
				{ "IsImmediate", new() { Type = "Boolean",  } },
            },
          
         */

        // Anything to do?
        if (type.GlobalSettings.Count == 0)
        {
            return;
        }

        WriteLineIndented("GlobalSettings = { ");
        Indent();

        foreach (var property in type.GlobalSettings)
        {
            WriteProperty(type, property.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes the specified property.
    /// </summary>
    /// <param name="type">The current type being serialized.</param>
    /// <param name="property">The property to serialize.</param>
    public void WriteProperty(DocumentedType type, DocumentedProperty property)
    {
        WriteIndented("{ ");
        Write($"\"{property.Name}\", Properties[\"{property.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified field.
    /// </summary>
    /// <param name="type">The current type being serialized.</param>
    /// <param name="field">The property to serialize.</param>
    public void WriteField(DocumentedType type, DocumentedField field)
    {
        WriteIndented("{ ");
        Write($"\"{field.Name}\", Fields[\"{field.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Serializes the specified methods.
    /// </summary>
    /// <param name="methods">The methods to serialize.</param>
    public void WriteMethods(IDictionary<string, DocumentedMethod> methods)
    {
        WriteLineIndented("// Build all of the documented methods");
        WriteLineIndented($"Methods = new Dictionary<string, DocumentedMethod>();");

        foreach (var method in methods)
        {
            // Skip excluded methods and internally generated methods
            if (!ApiDocumentationBuilder.ExcludedMethods.Contains(method.Value.Name) && !method.Value.Name.StartsWith('<'))
            {
                WriteMethod(method.Value);
            }
        }

        WriteLine();
    }

    /// <summary>
    /// Serializes the specified methods.
    /// </summary>
    /// <param name="type">The type containing the methods.</param>
    /// <param name="properties">The methods to serialize.</param>
    public void WriteMethods(DocumentedType type)
    {
        /* Example:

           Methods = { 
               { "SetValue", new() { Type = "Guid", Summary = "Gets the ID of the JavaScript listener.",  } },
               { "BrowserWindowSize", new() { Type = "BrowserWindowSize", Summary = "Gets the browser window size.",  } },
               { "Breakpoint", new() { Type = "Breakpoint", Summary = "Gets the breakpoint associated with the browser size.",  } },
               { "IsImmediate", new() { Type = "Boolean",  } },
           },

        */

        WriteLineIndented("Methods = { ");
        Indent();

        foreach (var method in type.Methods)
        {
            // Skip excluded methods and internally generated methods
            if (!ApiDocumentationBuilder.ExcludedMethods.Contains(method.Value.Name) && !method.Value.Name.StartsWith('<'))
            {
                WriteMethod(type, method.Value);
            }
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Serializes a documented method.
    /// </summary>
    /// <param name="method"></param>
    public void WriteMethod(DocumentedMethod method)
    {
        WriteIndented($"Methods.Add(\"{method.Key}\", new()");
        Write(" { ");
        Write($"Name = \"{method.Name}\", ");
        WriteReturnType(method);
        WriteDeclaringType(method);
        WriteCategory(method.Category);
        WriteOrder(method.Order);
        WriteSummary(method.Summary);
        WriteRemarks(method.Remarks);
        Write(" }");
        WriteLine(");");
    }

    /// <summary>
    /// Serializes the specified method.
    /// </summary>
    /// <param name="type">The current type being serialized.</param>
    /// <param name="method">The method to serialize.</param>
    public void WriteMethod(DocumentedType type, DocumentedMethod method)
    {
        WriteIndented("{ ");
        Write($"\"{method.Name}\", Methods[\"{method.Key}\"]");
        WriteLine(" },");
    }

    /// <summary>
    /// Writes whether the type inherits from <see cref="MudComponentBase"/>.
    /// </summary>
    /// <param name="isComponent"></param>
    public void WriteIsComponentIndented(bool isComponent)
    {
        if (isComponent)
        {
            WriteIndent();
            WriteLine($"IsComponent = true, ");
        }
    }

    /// <summary>
    /// Writes the type in which the property was declared, if it's another type.
    /// </summary>
    /// <param name="type">The type containing the property.</param>
    /// <param name="method">The property being described.</param>
    public void WriteReturnType(DocumentedMethod method)
    {
        Write($"TypeName = \"{Escape(method.Type.Name)}\", ");
        Write($"TypeFriendlyName = \"{GetFriendlyTypeName(method.Type)}\", ");
    }

    /// <summary>
    /// Writes whether a property is a parameter.
    /// </summary>
    /// <param name="isParameter"></param>
    public void WriteIsParameter(bool isParameter)
    {
        if (isParameter)
        {
            Write($"IsParameter = true, ");
        }
    }

    /// <summary>
    /// Writes the name of the given base type.
    /// </summary>
    /// <param name="baseType"></param>
    public void WriteBaseTypeIndented(Type baseType)
    {
        if (baseType != null)
        {
            WriteLineIndented($"BaseTypeName = \"{baseType.Name}\", ");
        }
    }

    /// <summary>
    /// Writes the declaring type of an event.
    /// </summary>
    /// <param name="documentedEvent">The event to serialize.</param>
    public void WriteDeclaringType(DocumentedEvent documentedEvent)
    {
        Write($"DeclaringTypeName = \"{Escape(documentedEvent.DeclaringTypeFullName)}\", ");
    }

    /// <summary>
    /// Writes the declaring type of a property.
    /// </summary>
    /// <param name="property">The property to serialize.</param>
    public void WriteDeclaringType(DocumentedProperty property)
    {
        Write($"DeclaringTypeName = \"{Escape(property.DeclaringTypeFullName)}\", ");
    }

    /// <summary>
    /// Writes the declaring type of a property.
    /// </summary>
    /// <param name="field">The property to serialize.</param>
    public void WriteDeclaringType(DocumentedField field)
    {
        Write($"DeclaringTypeName = \"{Escape(field.DeclaringTypeFullName)}\", ");
    }

    /// <summary>
    /// Writes the type in which the property was declared, if it's another type.
    /// </summary>
    /// <param name="type">The type containing the property.</param>
    /// <param name="method">The property being described.</param>
    public void WriteDeclaringType(DocumentedMethod method)
    {
        Write($"DeclaringTypeName = \"{Escape(method.DeclaringTypeFullName)}\", ");
    }

    /// <summary>
    /// Serializes all fields for the specified type.
    /// </summary>
    /// <param name="type">The type being serialized.</param>
    public void WriteFields(DocumentedType type)
    {
        if (type.Fields.Count == 0)
        {
            return;
        }

        WriteLineIndented("Fields = { ");
        Indent();

        foreach (var field in type.Fields)
        {
            WriteField(type, field.Value);
        }

        Outdent();
        WriteLineIndented("},");
    }

    /// <summary>
    /// Gets the C# equivalent of the specified XML type.
    /// </summary>
    /// <param name="fullName">The type name to convert.</param>
    /// <returns></returns>
    public static string GetFriendlyTypeName(Type type)
    {
        // Replace value types
        var name = type.FullName switch
        {
            "System.Boolean" => "bool",
            "System.Boolean[]" => "bool[]",
            "System.Int32" => "int",
            "System.Int32[]" => "int[]",
            "System.Int64" => "long",
            "System.Int64[]" => "long[]",
            "System.String" => "string",
            "System.String[]" => "string[]",
            "System.Double" => "double",
            "System.Double[]" => "double[]",
            "System.Single" => "float",
            "System.Single[]" => "float[]",
            "System.Object" => "object",
            "System.Void" => "",
            _ => type.Name
        };

        // Replace generics
        if (type.IsGenericType)
        {
            // Get the parameters
            var parameters = type.GetGenericArguments();
            // Shave off the `1
            name = string.Concat(name.AsSpan(0, name.Length - 2), "<");
            // Simplify all generic parameter
            for (var index = 0; index < parameters.Length; index++)
            {
                if (index > 0)
                {
                    name += ", ";
                }

                name += GetFriendlyTypeName(parameters[index]);
            }
            name += ">";
        }

        // Simplify Nullable<T> to T?
        foreach (var match in NullableRegEx().Matches(name).Cast<Match>())
        {
            name = name.Replace(match.Groups[0].Value, match.Groups[1].Value + "?");
        }

        return name;
    }

    /// <summary>
    /// The regular expression for Nullable<T>
    /// </summary>
    /// <returns></returns>
    [GeneratedRegex("Nullable<([\\S]*)>")]
    private static partial Regex NullableRegEx();
}
